/**
	httpmain.cpp
	~~~~~~~~~~~~~~~~~~~~~~~~~


	Copyright (c) 2007-2008 <ptthippo@gmail.com>

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
*/
#include "../common/config.h"
#include <fstream>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/convenience.hpp>
#include <boost/filesystem/exception.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/variables_map.hpp>
#include <boost/program_options/parsers.hpp>
#include "../common/logmanager.h"
#include "httpproxy.h"
#include "httpxmlcfg.h"

#ifdef UNIX
#include <grp.h>
#include <pwd.h>
#include <unistd.h>
#include <sys/types.h>
#include "../common/signal.h"
#endif

#ifdef WIN32
#include <windows.h>
#include <process.h>
#include <tchar.h>
#endif

#ifdef GUI
#include "../gui/WebUI.h"
#endif

using namespace std;
using namespace log4cxx;
namespace fs = boost::filesystem;
namespace po = boost::program_options;

#ifdef GUI
using namespace Wt;
#endif

bool start();

#ifdef GUI
WApplication *createApplication(const WEnvironment& env)
{
	return new WebUI(env);
}

int WRunThread()
{
	HTTPConfig& config = HTTPProxy::getInstance().getConfig();

	if(config.isWebEnabled() == false)
		return 1;

	string docroot, address, port;

	docroot = config.getWebDocRoot();
	address = config.getWebListeningAddress().to_string();
	port = boost::lexical_cast<string>(config.getWebListeningPort());
	
	int argc = 7;
	char* argv[7];// = {"xrelayer.exe", "--docroot", ".", "--http-address", "0.0.0.0", "--http-port", "777"};

	argv[0] = "xrelayer";
	argv[1] = "--docroot";
	char *p_docroot = new char[docroot.size()+1];
	strcpy(p_docroot, docroot.c_str());
	argv[2] = p_docroot;
	argv[3] = "--http-address";
	char *p_address = new char[address.size()+1];
	strcpy(p_address, address.c_str());
	argv[4] = p_address;
	argv[5] = "--http-port";
	char *p_port = new char[port.size()+1];
	strcpy(p_port, port.c_str());
	argv[6] = p_port;

	return WRun(argc, argv, &createApplication);
}

#endif

#ifdef UNIX

int chuser()
{
	HTTPConfig& config = HTTPProxy::getInstance().getConfig();
	string username, groupname;
	LoggerPtr logger = Logger::getLogger("httpproxy");

	username = config.getProcUsername();
	groupname = config.getProcGroupname();

	if(groupname.size() > 0)
	{
		struct group *thisgroup = NULL;
		thisgroup = getgrnam(groupname.c_str());
		if (!thisgroup)
		{
			LOG4CXX_FATAL(logger, "cannot get groupname " + groupname);
			return -1;
		}
		
		if (setgid(thisgroup->gr_gid) < 0)
		{
			LOG4CXX_FATAL(logger, "cannot change to group " + groupname);
			return -2;
		}

		LOG4CXX_INFO(Logger::getRootLogger(), "change to group " + groupname);
	}

	if(username.size() > 0)
	{
		struct passwd *thisuser = getpwnam(username.c_str());
		if (!thisuser)
		{
			LOG4CXX_FATAL(logger, "cannot get username " + username);
			return -3;
		}
		if (setuid(thisuser->pw_uid) < 0)
		{
			LOG4CXX_FATAL(logger, "cannot change to user " + username);
			return -4;
		}

		LOG4CXX_INFO(logger, "change to user " + username);
	}

	return 1;
}

void makedaemon()
{
	if (fork() != 0)
		exit(0);

	setsid();
	set_signal_handler(SIGHUP, SIG_IGN);

	if (fork() != 0)
		exit(0);

#ifdef GUI
	// We have to change the working directory to the one that contains GUI resources (like images, etc)
	char pathbuf[1024];
	char chpath[1024];
	ssize_t len;

	strcpy(chpath, "/");
	if ((len = readlink("/proc/self/exe", pathbuf, sizeof(pathbuf)-1)) != -1)
		pathbuf[len] = '\0';

	if(len > 0)
	{
		char* idx = strrchr(pathbuf, (int)'/');
		if(idx)
		{
			*idx = 0;
			strcpy(chpath, pathbuf);
		}
	}

	chdir(chpath);
#else
	chdir("/");
#endif

	umask(077);

#ifdef NDEBUG
	close(0);
	close(1);
	close(2);
#endif
}
#endif

#ifdef WIN32

#define SZSERVICEDISPLAYNAME "XRelayer HTTP Proxy"
#define SZSERVICENAME "XRelayer"

SERVICE_STATUS_HANDLE   sshStatusHandle;
SERVICE_STATUS ssStatus;

LPTSTR GetLastErrorText( LPTSTR lpszBuf, DWORD dwSize )
{
	DWORD dwRet;
	LPTSTR lpszTemp = NULL;

	dwRet = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |FORMAT_MESSAGE_ARGUMENT_ARRAY,
							NULL,
							GetLastError(),
							LANG_NEUTRAL,
							(LPTSTR)&lpszTemp,
							0,
							NULL );

	// supplied buffer is not long enough
	if ( !dwRet || ( (int)dwSize < (int)dwRet+14 ) )
		lpszBuf[0] = TEXT('\0');
	else
	{
		lpszTemp[lstrlen(lpszTemp)-2] = TEXT('\0');  //remove cr and newline character
		_stprintf( lpszBuf, TEXT("%s (0x%x)"), lpszTemp, GetLastError() );
	}

	if ( lpszTemp )
		LocalFree((HLOCAL) lpszTemp );

	return lpszBuf;
}

void AddToMessageLog(LPTSTR lpszMsg)
{
	TCHAR   szMsg[256];
	HANDLE  hEventSource;
	LPTSTR  lpszStrings[2];


	DWORD dwErr = GetLastError();

	// Use event logging to log the error.
	//
	hEventSource = RegisterEventSource(NULL, TEXT(SZSERVICENAME));

	_stprintf(szMsg, TEXT("%s error: %d"), TEXT(SZSERVICENAME), dwErr);
	lpszStrings[0] = szMsg;
	lpszStrings[1] = lpszMsg;

	if (hEventSource != NULL)
	{
	    ReportEvent(hEventSource, // handle of event source
			EVENTLOG_ERROR_TYPE,  // event type
			0,                    // event category
			0,                    // event ID
			NULL,                 // current user's SID
			2,                    // strings in lpszStrings
			0,                    // no bytes of raw data
			(LPCTSTR *)lpszStrings,          // array of error strings
			NULL);                // no raw data

		(VOID) DeregisterEventSource(hEventSource);
	}

}

bool ReportStatusToSCMgr(DWORD dwCurrentState,
				                         DWORD dwWin32ExitCode,
				                         DWORD dwWaitHint)
{
	static DWORD dwCheckPoint = 1;
	bool fResult = true;

	if (dwCurrentState == SERVICE_START_PENDING)
		ssStatus.dwControlsAccepted = 0;
	else
		ssStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;

	ssStatus.dwCurrentState = dwCurrentState;
	ssStatus.dwWin32ExitCode = dwWin32ExitCode;
	ssStatus.dwWaitHint = dwWaitHint;

	if ( ( dwCurrentState == SERVICE_RUNNING ) ||
			( dwCurrentState == SERVICE_STOPPED ) )
		ssStatus.dwCheckPoint = 0;
	else
		ssStatus.dwCheckPoint = dwCheckPoint++;


	// Report the status of the service to the service control manager.
	//
	if (!(fResult = SetServiceStatus(sshStatusHandle, &ssStatus)))
	{
		AddToMessageLog(TEXT("SetServiceStatus"));
	}

	return fResult;
}

void CmdInstallService(bool auto_start)
{
	SC_HANDLE   schService;
	SC_HANDLE   schSCManager;

	DWORD start_type;
	TCHAR szPath[512];
	TCHAR szErr[256];

	if ( GetModuleFileName( NULL, szPath, 512 ) == 0 )
	{
		_tprintf(TEXT("Unable to install %s - %s\n"), TEXT(SZSERVICEDISPLAYNAME), GetLastErrorText(szErr, 256));
		return;
	}

	if(auto_start)
		start_type = SERVICE_AUTO_START;
	else
		start_type = SERVICE_DEMAND_START;
	sprintf(szPath, "%s -c config.xml -l log.conf --runservice", szPath);

	schSCManager = OpenSCManager(
						NULL,                   // machine (NULL == local)
						NULL,                   // database (NULL == default)
						SC_MANAGER_ALL_ACCESS   // access required
						);
	if ( schSCManager )
	{
		schService = CreateService(
			schSCManager,               // SCManager database
			TEXT(SZSERVICENAME),        // name of service
			TEXT(SZSERVICEDISPLAYNAME), // name to display
			SERVICE_ALL_ACCESS,         // desired access
			SERVICE_WIN32_OWN_PROCESS,  // service type
			start_type,       // start type
			SERVICE_ERROR_NORMAL,       // error control type
			szPath,                     // service's binary
			NULL,                       // no load ordering group
			NULL,                       // no tag identifier
			"",       // dependencies
			NULL,                       // LocalSystem account
			NULL);                      // no password

		if ( schService )
		{
			_tprintf(TEXT("%s installed.\n"), TEXT(SZSERVICEDISPLAYNAME) );
			CloseServiceHandle(schService);
		}
		else
		{
			_tprintf(TEXT("CreateService failed - %s\n"), GetLastErrorText(szErr, 256));
		}

		CloseServiceHandle(schSCManager);
	}
	else
		_tprintf(TEXT("OpenSCManager failed - %s\n"), GetLastErrorText(szErr,256));
}

void CmdRemoveService()
{
	SC_HANDLE   schService;
	SC_HANDLE   schSCManager;

	TCHAR szErr[256];

	schSCManager = OpenSCManager(
						NULL,                   // machine (NULL == local)
						NULL,                   // database (NULL == default)
						SC_MANAGER_ALL_ACCESS   // access required
						);
	if ( schSCManager )
	{
		schService = OpenService(schSCManager, TEXT(SZSERVICENAME), SERVICE_ALL_ACCESS);

		if (schService)
		{
			// try to stop the service
			if ( ControlService( schService, SERVICE_CONTROL_STOP, &ssStatus ) )
			{
				_tprintf(TEXT("Stopping %s."), TEXT(SZSERVICEDISPLAYNAME));
				Sleep( 1000 );

				while( QueryServiceStatus( schService, &ssStatus ) )
				{
					if ( ssStatus.dwCurrentState == SERVICE_STOP_PENDING )
					{
						_tprintf(TEXT("."));
						Sleep( 1000 );
					}
					else
						break;
				}

				if ( ssStatus.dwCurrentState == SERVICE_STOPPED )
					_tprintf(TEXT("\n%s stopped.\n"), TEXT(SZSERVICEDISPLAYNAME) );
				else
					_tprintf(TEXT("\n%s failed to stop.\n"), TEXT(SZSERVICEDISPLAYNAME) );
			}

			// now remove the service
			if( DeleteService(schService) )
				_tprintf(TEXT("%s removed.\n"), TEXT(SZSERVICEDISPLAYNAME) );
			else
				_tprintf(TEXT("DeleteService failed - %s\n"), GetLastErrorText(szErr,256));

			CloseServiceHandle(schService);
		}
		else
			_tprintf(TEXT("OpenService failed - %s\n"), GetLastErrorText(szErr,256));

		CloseServiceHandle(schSCManager);
	}
	else
		_tprintf(TEXT("OpenSCManager failed - %s\n"), GetLastErrorText(szErr,256));
}

void WINAPI ServiceCtrl(DWORD dwCtrlCode)
{
	// Handle the requested control code.
	//
	switch(dwCtrlCode)
	{
	// Stop the service.
	//
	case SERVICE_CONTROL_SHUTDOWN:
	case SERVICE_CONTROL_STOP:
		HTTPProxy::getInstance().StopProxy();
		ReportStatusToSCMgr(SERVICE_STOP_PENDING, NO_ERROR, 30000);  

		break;

	// Update the service status.
	//
	case SERVICE_CONTROL_INTERROGATE:
		break;

	// invalid control code
	//
	default:
		break;
	}

}

void WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
	DWORD dwErr;
	
	// register our service control handler:
	//
	sshStatusHandle = RegisterServiceCtrlHandler(TEXT(SZSERVICENAME), ServiceCtrl);

	if (!sshStatusHandle)
	{
		dwErr = GetLastError();	
		ReportStatusToSCMgr(SERVICE_STOPPED, dwErr, 0);
		return;
	}


	ssStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	ssStatus.dwServiceSpecificExitCode = 0;

	// report the status to the service control manager.
	//
	if (!ReportStatusToSCMgr(
			SERVICE_START_PENDING, // service state
			NO_ERROR,              // exit code
			3000))                 // wait hint
	{
		dwErr = GetLastError();	
		ReportStatusToSCMgr(SERVICE_STOPPED, dwErr, 0);
		return;
	}

	start();
	if(sshStatusHandle)
	{
		ReportStatusToSCMgr(SERVICE_RUNNING, NO_ERROR,	 0);
	}
	
	HTTPProxy::getInstance().join();

	if(sshStatusHandle)
	{
		ReportStatusToSCMgr(SERVICE_STOPPED, NO_ERROR,	 0);
	}

	return;
}

#endif

bool start()
{
	HTTPProxy::getInstance().Initialize();
	HTTPProxy::getInstance().StartProxy();

#ifdef GUI
	boost::thread *wt_thread = new boost::thread(&WRunThread);
#endif

	return true;
}

#ifdef WIN32
#define DEFAULT_CONFIG	"config.xml"
#else
#define DEFAULT_CONFIG	"/etc/xrelayer.conf"
#endif

int main(int argc, char** argv)
{
	po::options_description available_options("Available options");

	string config_filename;
	string logconfig_filename;

#ifdef WIN32
	SERVICE_TABLE_ENTRY dispatchTable[] =
	{
		{ TEXT(SZSERVICENAME), (LPSERVICE_MAIN_FUNCTION)ServiceMain },
		{ NULL, NULL }
	};
#endif

	available_options.add_options()
		("config,c",
		po::value<string>(&config_filename)->default_value(DEFAULT_CONFIG), "xml config file")
		("logconfig,l", po::value<string>(&logconfig_filename), "log config file")
		("help,h", "print this help message")
		;

#ifdef WIN32
	po::options_description win32_options("Windows specific options");
	win32_options.add_options()
		("installservice", "install this program as a windows service")
		("autostart", "automatically start the service when windows boots")
		("removeservice", "remove this program from windows service manager")
		("runservice", "run as a windows service")
		;
	available_options.add(win32_options);
#endif

	po::variables_map vm;
	try
	{	
		po::store(po::parse_command_line(argc, argv, available_options), vm);
	}
	catch(const exception & e)
	{
		cout << e.what() << endl;
		return 0;
	}
	po::notify(vm);

	
	if (vm.count("help"))
	{
		cout << available_options << endl;
		return 1;
	}

#ifdef WIN32
	if(vm.count("installservice"))
	{
		if(vm.count("autostart"))
			CmdInstallService(true);
		else
			CmdInstallService(false);
		return 1;
	}

	if(vm.count("removeservice"))
	{
		CmdRemoveService();
		return 1;
	}
#endif

#ifdef WIN32
	if(vm.count("runservice"))
	{
		char curPath[1024];
		char *pSep = NULL;
		GetModuleFileName(NULL, curPath, 1023);
		curPath[1023] = NULL;
		if((pSep = strrchr(curPath, '\\')) != NULL)
			*pSep = '\0';

		SetCurrentDirectory(curPath);
	}
#endif

	HTTPXMLCfg xml_config;

	// set hostname prior to loading from config
	char hostname[512];
	if(gethostname(hostname, sizeof(hostname)) == 0)
	{
		xml_config.setHostname(string(hostname));
	}
	else
	{
		xml_config.setHostname(string("unknown"));
	}

	fs::path config_path(config_filename);
	if(fs::exists(config_path) == false)
	{
		cout << "config file " << config_filename << " is not found." << endl;
	}
	else
	{
		if(xml_config.load(config_filename) != HTTPConfig::SUCCESS)
		{
			cout << "config file " << config_filename.c_str() << " cannot be loaded" << endl;
			return 0;
		}
	}

	HTTPProxy::getInstance().setConfig(xml_config);
#ifdef USE_LOG4CXX
	LogManager::configure();
#endif

#ifdef WIN32
	if(vm.count("runservice"))
	{
		if (!StartServiceCtrlDispatcher(dispatchTable))
			AddToMessageLog(TEXT("StartServiceCtrlDispatcher failed."));
	}
	else
	{
#endif

#ifdef UNIX
	makedaemon();
	chuser();
#endif
	
	start();
	HTTPProxy::getInstance().join();

#ifdef WIN32
	}
#endif
	
	return 1;
}


